
import 'dart:ui';

import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/flame.dart';
import 'package:klondike/klondike/klondike_game.dart';

import '../components/card.dart';
import '../components/flat_button.dart';
import '../components/operation.dart';
import '../components/pile.dart';
import '../enums/action.dart';
import '../util/effect_utils.dart';
import 'components/foundation_pile.dart';
import 'components/stock_pile.dart';
import 'components/tableau_pile.dart';
import 'components/waste_pile.dart';

class KlondikeWorld extends World with HasGameReference<KlondikeGame> {

  static const double cardWidth = KlondikeGame.cardWidth;
  static const double cardHeight = KlondikeGame.cardHeight;
  static const double cardGap = KlondikeGame.cardGap;
  static const double cardRadius = KlondikeGame.cardRadius;
  static final Vector2 cardSize = KlondikeGame.cardSize;

  static const double topGap = KlondikeGame.topGap;
  static const double cardSpaceWidth = KlondikeGame.cardSpaceWidth;
  static const double cardSpaceHeight = KlondikeGame.cardSpaceHeight;

  static const double dragTolerance = KlondikeGame.dragTolerance;

  static final cardRRect = RRect.fromRectAndRadius(
    const Rect.fromLTWH(0, 0, cardWidth, cardHeight),
    const Radius.circular(cardRadius),
  );

  late Vector2 playAreaSize;

  final stock = StockPile(position: Vector2(0.0, 0.0));
  final waste = WastePile(position: Vector2(0.0, 0.0));
  final List<FoundationPile> foundations = [];
  final List<TableauPile> tableauPiles = [];
  final List<Card> cards = [];

  final List<Operation> histories = [];
  final List<Pile> canTipPiles = [];

  @override
  Future<void> onLoad() async {

    // WidgetsFlutterBinding.ensureInitialized();

    await Flame.device.fullScreen();
    await Flame.device.setLandscape();

    for(var i=1; i<=13; i++) {
      await Flame.images.load('${i}_of_clubs.png');
      await Flame.images.load('${i}_of_diamonds.png');
      await Flame.images.load('${i}_of_hearts.png');
      await Flame.images.load('${i}_of_spades.png');
    }
    await Flame.images.load('klondike-sprites.png');
    await Flame.images.load('xiaoxin_background.png');

    stock.position = Vector2(cardGap, topGap);
    waste.position = Vector2(cardWidth + 2 * cardGap, topGap);

    foundations.addAll(List.generate(
      4,
      (i) => FoundationPile(i, checkWin)
        ..position =
            Vector2((i + 3) * (cardWidth + cardGap) + cardGap, topGap),
    ));

    tableauPiles.addAll(List.generate(
      7,
      (i) => TableauPile()
        ..position = Vector2(
          cardGap + i * (cardWidth + cardGap),
          1.2 * cardHeight + topGap,
        ),
    ));

    add(stock);
    add(waste);
    addAll(foundations);
    addAll(tableauPiles);

    canTipPiles.add(waste);
    canTipPiles.addAll(tableauPiles);


    // camera
    game.camera.viewfinder.visibleGameSize = 
      Vector2(cardWidth * 7 + cardGap * 8, cardHeight * 4 + cardGap * 3);
    game.camera.viewfinder.position = Vector2(cardWidth * 3.5 + cardGap * 4, 0);
    game.camera.viewfinder.anchor = Anchor.topCenter;

    cards.addAll([
      for(var rank = 1; rank <= 13; rank++)
        for(var suit = 0; suit < 4; suit++)
          Card(rank, suit)
    ]);
    
    cards.shuffle();
    addAll(cards);

    playAreaSize =
        Vector2(7 * cardSpaceWidth + cardGap, 4 * cardSpaceHeight + topGap);
    final gameMidX = playAreaSize.x / 2;

    addButton('再来一局', gameMidX + 2 * cardSpaceWidth, Action.newDeal);
    addButton('改变刷新个数', gameMidX + 3 * cardSpaceWidth, Action.changeDraw);
    addButton('撤销', gameMidX + cardSpaceWidth, Action.revocation);
    addButton('提示', gameMidX, Action.tips);

    deal();
  }


  void addButton(String label, double buttonX, Action action) {
    final button = FlatButton(
      label,
      size: Vector2(cardWidth, 0.6 * topGap),
      position: Vector2(buttonX, topGap / 2),
      onReleased: () async {
        game.action = action;
        switch(action) {
          case Action.newDeal: 
          case Action.changeDraw: 
            game.world = KlondikeWorld();
            break;
          
          case Action.revocation:
            if(histories.isNotEmpty) {
              histories.removeLast().revoke();
            }
            break;
          case Action.tips:
            await tips();
            break;
        }
      },
    );
    add(button);
  }

  Future<void> deal() async {
    assert(cards.length == 52, 'There are ${cards.length} cards: should be 52');

    if (game.action == Action.changeDraw) {
      game.fanoutNum = (game.fanoutNum == 3) ? 1 : 3;
    }

    cards.shuffle();

    for(Card card in cards) {
      card.position = Vector2(cardGap, topGap);
    }

    var dealPriority = 1;
    for (final card in cards) {
      card.priority = dealPriority++;
    }

    var cardToDeal = cards.length - 1;
    for (var i = 0; i < 7; i++) {

      for (var j = 0; j <= i; j++) {
        final card = cards[cardToDeal--];
        await EffectUtils.doMove(
          [card],
          [tableauPiles[i].position],
          speed: 60.0,
          start: 0
        );

        tableauPiles[i].acquireCard([card]);
        if (j == i) {
          tableauPiles[i].flipTopCard();
        }
      }
    }

    stock.acquireCard(cards.sublist(0, cardToDeal+1));
  }

  Future<void> tips() async {
    canTipPiles.shuffle();
    for(Pile pile in canTipPiles) {
      if(await pile.tryToMove()) {
        break;
      }
    }
  }


  void checkWin() {
    // Callback from a Foundation Pile when it is full (Ace to King).
    var nComplete = 0;
    for (final f in foundations) {
      if (f.isFull) {
        nComplete++;
      }
    }
    if (nComplete == foundations.length) {
      letsCelebrate();
    }
  }

  Future<void> letsCelebrate({int phase = 1}) async {
    // Deal won: bring all cards to the middle of the screen (phase 1)
    // then scatter them to points just outside the screen (phase 2).
    //
    // First get the device's screen-size in game co-ordinates, then get the
    // top-left of the off-screen area that will accept the scattered cards.
    // Note: The play area is anchored at TopCenter, so topLeft.y is fixed.

    final cameraZoom = game.camera.viewfinder.zoom;
    final zoomedScreen = game.size / cameraZoom;
    final screenCenter = (playAreaSize - cardSize) / 2;
    final topLeft = Vector2(
      (playAreaSize.x - zoomedScreen.x) / 2 - cardWidth,
      -cardHeight,
    );
    final nCards = cards.length;
    final offscreenHeight = zoomedScreen.y + cardSize.y;
    final offscreenWidth = zoomedScreen.x + cardSize.x;
    final spacing = 2.0 * (offscreenHeight + offscreenWidth) / nCards;

    // Starting points, directions and lengths of offscreen rect's sides.
    final corner = [
      Vector2(0.0, 0.0),
      Vector2(0.0, offscreenHeight),
      Vector2(offscreenWidth, offscreenHeight),
      Vector2(offscreenWidth, 0.0),
    ];
    final direction = [
      Vector2(0.0, 1.0),
      Vector2(1.0, 0.0),
      Vector2(0.0, -1.0),
      Vector2(-1.0, 0.0),
    ];
    final length = [
      offscreenHeight,
      offscreenWidth,
      offscreenHeight,
      offscreenWidth,
    ];

    var side = 0;
    var offScreenPosition = corner[side] + topLeft;
    var space = length[side];

    List<Vector2> destinations = [];
    for(int i=0; i<cards.length; i++) {
      Card card = cards[i];
      card.priority = i;
      if (card.isFaceDown) {
        card.flip();
      }
      if(phase == 1) {
        destinations.add(screenCenter);
      } else {
        destinations.add(offScreenPosition);
        offScreenPosition = offScreenPosition + direction[side] * spacing;
        space = space - spacing;
        if ((space < 0.0) && (side < 3)) {
          // Out of space: change to the next side and use excess spacing there.
          side++;
          offScreenPosition = corner[side] + topLeft - direction[side] * space;
          space = length[side] + space;
        }
      }
    }
    await EffectUtils.doMove(cards, destinations, speed: 5.0);

    if (phase == 1) {
        letsCelebrate(phase: 2);
    } else {
      game.action = Action.newDeal;
      game.world = KlondikeWorld();
    }
  }


  Future<void> highlight(List<PositionComponent> from, List<PositionComponent> to) async {
    var effects = [];

    for(int i=0; i<from.length; i++) {
      var effect = OpacityEffect.to(
        0.8,
        EffectController(duration: 1),
      );
      var component = RectangleComponent(
        position: from[i].position,
        size: from[i].size,
        anchor: from[i].anchor,
        priority: from[i].priority + 1,
      )..add(effect)..add(RemoveEffect(delay: 1));
      add(component);
      effects.add(effect);
    }

    for(int i=0; i<to.length; i++) {
      var effect = OpacityEffect.to(
        0.8,
        EffectController(startDelay: 1, duration: 1),
      );
      var component = RectangleComponent(
        position: to[i].position,
        size: cardSize,
        anchor: to[i].anchor,
        priority: to[i].priority + 1,
      )..add(effect)..add(RemoveEffect(delay: 2));
      component.opacity = 0.0;
      add(component);
      effects.add(effect);
    }

    for (var effect in effects) {
      await effect.completed;
    }
  }
}